Ознайомтеся з декораторами JavaScript: потужною функцією метапрограмування для додавання метаданих та реалізації патернів AOP. Дізнайтеся, як покращити повторне використання коду, читабельність та зручність обслуговування з практичними прикладами.
Декоратори JavaScript: програмування метаданих та патерни AOP
Декоратори JavaScript — це потужна та виразна функція метапрограмування, яка дозволяє змінювати або покращувати поведінку класів, методів, властивостей та параметрів у декларативний та багаторазовий спосіб. Вони забезпечують стислий синтаксис для додавання метаданих та реалізації принципів аспектно-орієнтованого програмування (AOP), покращуючи повторне використання коду, читабельність та зручність обслуговування. Цей всеосяжний посібник детально розгляне декоратори JavaScript, охоплюючи їх синтаксис, використання та застосування в різних сценаріях. Хоча офіційно це все ще пропозиція, що розвивається, декоратори широко використовуються, особливо в таких фреймворках, як Angular та NestJS, і їхній вплив на розробку JavaScript незаперечний.
Що таке декоратори JavaScript?
Декоратори — це спеціальний тип декларації, який можна прикріпити до декларації класу, методу, аксесора, властивості або параметра. Вони використовують форму @expression, де expression має обчислюватися як функція, яка буде викликана під час виконання з інформацією про декоровану декларацію. По суті, декоратори діють як функції, які обгортають або змінюють декорований елемент, дозволяючи додавати додаткові функції або метадані безпосередньо не змінюючи вихідний код.
Розглядайте декоратори як анотації або маркери, які можна прикріпити до елементів коду. Ці маркери потім можна обробляти під час виконання для виконання різних завдань, таких як реєстрація, перевірка, авторизація або впровадження залежностей. Декоратори сприяють чистішій і більш модульній структурі коду, розділяючи проблеми та зменшуючи шаблонний код.
Переваги використання декораторів
- Покращене повторне використання коду: Декоратори дозволяють інкапсулювати загальну поведінку в багаторазові компоненти, які можна застосовувати до кількох частин вашої програми. Це зменшує дублювання коду та сприяє узгодженості.
- Покращена читабельність: Розділяючи міжфункціональні проблеми на декоратори, ви можете зробити свою основну логіку чистішою та зрозумілішою. Декоратори забезпечують декларативний спосіб вираження додаткової поведінки, роблячи код більш самодокументованим.
- Підвищення зручності обслуговування: Декоратори сприяють модульності та розділенню проблем, полегшуючи зміну або розширення вашої програми, не впливаючи на інші частини кодової бази. Це зменшує ризик появи помилок і спрощує процес обслуговування.
- Аспектно-орієнтоване програмування (AOP): Декоратори дозволяють реалізовувати принципи AOP, дозволяючи впроваджувати поведінку в існуючий код, не змінюючи його вихідний код. Це особливо корисно для вирішення міжфункціональних проблем, таких як реєстрація, безпека та управління транзакціями.
Типи декораторів
Декоратори JavaScript можна застосовувати до різних типів декларацій, кожен з яких має власну специфічну мету та синтаксис:
Декоратори класів
Декоратори класів застосовуються до конструктора класу та можуть використовуватися для зміни визначення класу або додавання метаданих. Декоратор класу отримує конструктор класу як свій єдиний аргумент.
Приклад: Додавання метаданих до класу.
function Component(options: { selector: string, template: string }) {
return function (constructor: T) {
return class extends constructor {
selector = options.selector;
template = options.template;
}
}
}
@Component({ selector: 'my-component', template: 'Hello' })
class MyComponent {
constructor() {
// ...
}
}
console.log(new MyComponent().selector); // Output: my-component
У цьому прикладі декоратор Component додає властивості selector та template до класу MyComponent, дозволяючи вам налаштувати метадані компонента декларативним способом. Це аналогічно тому, як визначаються компоненти Angular.
Декоратори методів
Декоратори методів застосовуються до методів у класі та можуть використовуватися для зміни поведінки методу або додавання метаданих. Декоратор методу отримує три аргументи:
- Цільовий об’єкт (або прототип класу, або конструктор класу, залежно від того, чи є метод статичним).
- Назва методу.
- Дескриптор властивості для методу.
Приклад: Реєстрація викликів методів.
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey} with arguments: ${JSON.stringify(args)}`);
const result = originalMethod.apply(this, args);
console.log(`${propertyKey} returned: ${result}`);
return result;
}
return descriptor;
}
class Calculator {
@Log
add(a: number, b: number) {
return a + b;
}
}
const calculator = new Calculator();
calculator.add(2, 3); // Output: Calling add with arguments: [2,3]
// add returned: 5
У цьому прикладі декоратор Log реєструє виклик методу та його аргументи перед виконанням вихідного методу та реєструє значення, що повертається, після виконання. Це простий приклад того, як декоратори можна використовувати для реалізації функцій реєстрації або аудиту, не змінюючи основну логіку методу.
Декоратори властивостей
Декоратори властивостей застосовуються до властивостей у класі та можуть використовуватися для зміни поведінки властивості або додавання метаданих. Декоратор властивості отримує два аргументи:
- Цільовий об’єкт (або прототип класу, або конструктор класу, залежно від того, чи є властивість статичною).
- Назва властивості.
Приклад: Перевірка значень властивостей.
function Validate(target: any, propertyKey: string) {
let value: any;
const getter = function () {
return value;
};
const setter = function (newVal: any) {
if (typeof newVal !== 'number' || newVal < 0) {
throw new Error(`Invalid value for ${propertyKey}. Must be a non-negative number.`);
}
value = newVal;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true,
});
}
class Product {
@Validate
price: number;
constructor(price: number) {
this.price = price;
}
}
const product = new Product(10);
console.log(product.price); // Output: 10
try {
product.price = -5; // Throws an error
} catch (e) {
console.error(e.message);
}
У цьому прикладі декоратор Validate перевіряє властивість price, щоб переконатися, що це невід’ємне число. Якщо призначено недійсне значення, виникає помилка. Це простий приклад того, як декоратори можна використовувати для реалізації перевірки даних.
Декоратори параметрів
Декоратори параметрів застосовуються до параметрів методу та можуть використовуватися для додавання метаданих або зміни поведінки параметра. Декоратор параметра отримує три аргументи:
- Цільовий об’єкт (або прототип класу, або конструктор класу, залежно від того, чи є метод статичним).
- Назва методу.
- Індекс параметра в списку параметрів методу.
Приклад: Впровадження залежностей.
import 'reflect-metadata';
const Injectable = (): ClassDecorator => {
return (target: any) => {
Reflect.defineMetadata('injectable', true, target);
};
};
const Inject = (token: string): ParameterDecorator => {
return (target: any, propertyKey: string | symbol, parameterIndex: number) => {
let existingParameters: string[] = Reflect.getOwnMetadata('parameters', target, propertyKey) || [];
existingParameters[parameterIndex] = token;
Reflect.defineMetadata('parameters', existingParameters, target, propertyKey);
};
};
@Injectable()
class Logger {
log(message: string) {
console.log(`Logger: ${message}`);
}
}
class Greeter {
private logger: Logger;
constructor(@Inject('Logger') logger: Logger) {
this.logger = logger;
}
greet(name: string) {
this.logger.log(`Hello, ${name}!`);
}
}
// Simple dependency injection container
class Container {
private dependencies: Map = new Map();
register(token: string, dependency: any) {
this.dependencies.set(token, dependency);
}
resolve(target: any): T {
const parameters: string[] = Reflect.getMetadata('parameters', target) || [];
const resolvedDependencies = parameters.map(token => this.dependencies.get(token));
return new target(...resolvedDependencies);
}
}
const container = new Container();
container.register('Logger', new Logger());
const greeter = container.resolve(Greeter);
greeter.greet('World'); // Output: Logger: Hello, World!
У цьому прикладі декоратор Inject використовується для впровадження залежностей у конструктор класу Greeter. Декоратор пов’язує маркер із параметром, який потім можна використовувати для вирішення залежності за допомогою контейнера впровадження залежностей. У цьому прикладі показано базову реалізацію впровадження залежностей за допомогою декораторів і бібліотеки reflect-metadata.
Практичні приклади та випадки використання
Декоратори JavaScript можна використовувати в різних сценаріях для покращення якості коду та спрощення розробки. Ось кілька практичних прикладів і випадків використання:
Реєстрація та аудит
Декоратори можна використовувати для автоматичного запису викликів методів, аргументів і значень, що повертаються, надаючи цінну інформацію про поведінку та продуктивність програми. Це може бути особливо корисним для налагодження та усунення несправностей.
function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const startTime = performance.now();
console.log(`[${new Date().toISOString()}] Calling method: ${propertyKey} with arguments: ${JSON.stringify(args)}`);
const result = originalMethod.apply(this, args);
const endTime = performance.now();
const executionTime = endTime - startTime;
console.log(`[${new Date().toISOString()}] Method ${propertyKey} returned: ${result}. Execution time: ${executionTime.toFixed(2)}ms`);
return result;
};
return descriptor;
}
class ExampleClass {
@LogMethod
complexOperation(a: number, b: number): number {
// Simulate a time-consuming operation
let sum = 0;
for (let i = 0; i < 1000000; i++) {
sum += a + b + i;
}
return sum;
}
}
const example = new ExampleClass();
example.complexOperation(5, 10);
Цей розширений приклад вимірює час виконання методу та реєструє його разом із поточною позначкою часу, надаючи більш детальну інформацію для аналізу продуктивності.
Авторизація та автентифікація
Декоратори можна використовувати для забезпечення політик безпеки шляхом перевірки ролей і дозволів користувачів перед виконанням методу. Це може запобігти несанкціонованому доступу до конфіденційних даних і функцій.
function Authorize(role: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const userRole = getCurrentUserRole(); // Function to retrieve the current user's role
if (userRole !== role) {
throw new Error(`Unauthorized: User does not have the required role (${role}) to access this method.`);
}
return originalMethod.apply(this, args);
};
return descriptor;
};
}
function getCurrentUserRole(): string {
// In a real application, this would retrieve the user's role from authentication context
return 'admin'; // Example: Hardcoded role for demonstration
}
class AdminPanel {
@Authorize('admin')
deleteUser(userId: number) {
console.log(`User ${userId} deleted successfully.`);
}
@Authorize('editor')
editArticle(articleId: number) {
console.log(`Article ${articleId} edited successfully.`);
}
}
const adminPanel = new AdminPanel();
try {
adminPanel.deleteUser(123);
adminPanel.editArticle(456); // This will throw an error because the user role is 'admin'
} catch (error) {
console.error(error.message);
}
У цьому розширеному прикладі декоратор Authorize перевіряє, чи має поточний користувач вказану роль, перш ніж надати доступ до методу. Функція getCurrentUserRole (яка витягуватиме фактичну роль користувача в реальній програмі) використовується для визначення поточної ролі користувача. Якщо користувач не має необхідної ролі, виникає помилка, яка не дає змоги виконати метод.
Кешування
Декоратори можна використовувати для кешування результатів дорогих операцій, покращуючи продуктивність програми та зменшуючи навантаження на сервер. Це може бути особливо корисним для часто використовуваних даних, які не часто змінюються.
function Cache(ttl: number = 60) { // ttl in seconds, default to 60 seconds
const cache = new Map();
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
const cacheKey = `${propertyKey}-${JSON.stringify(args)}`;
const cachedData = cache.get(cacheKey);
if (cachedData && Date.now() < cachedData.expiry) {
console.log(`Retrieving from cache: ${propertyKey} with arguments: ${JSON.stringify(args)}`);
return cachedData.data;
}
console.log(`Executing and caching: ${propertyKey} with arguments: ${JSON.stringify(args)}`);
const result = await originalMethod.apply(this, args);
cache.set(cacheKey, {
data: result,
expiry: Date.now() + ttl * 1000, // Calculate expiry time
});
return result;
};
return descriptor;
};
}
class DataService {
@Cache(120) // Cache for 120 seconds
async fetchData(id: number): Promise {
// Simulate fetching data from a database or API
return new Promise((resolve) => {
setTimeout(() => {
resolve(`Data for ID ${id} fetched from source.`);
}, 1000); // Simulate a 1-second delay
});
}
}
const dataService = new DataService();
(async () => {
console.log(await dataService.fetchData(1)); // Executes the method
console.log(await dataService.fetchData(1)); // Retrieves from cache
await new Promise(resolve => setTimeout(resolve, 121000)); // Wait for 121 seconds to allow the cache to expire
console.log(await dataService.fetchData(1)); // Executes the method again after cache expiry
})();
У цьому розширеному прикладі реалізовано базовий механізм кешування за допомогою Map. Декоратор Cache зберігає результати декорованого методу протягом визначеного часу життя (TTL). Коли метод викликається знову з тими самими аргументами, повертається кешований результат замість повторного виконання методу. Після закінчення терміну дії TTL метод виконується знову, і результат кешується.
Перевірка
Декоратори можна використовувати для перевірки даних перед їх обробкою, забезпечуючи цілісність даних і запобігаючи помилкам. Це може бути особливо корисним для перевірки введення даних користувачем або даних, отриманих із зовнішніх джерел.
function Required() {
return function (target: any, propertyKey: string) {
if (!target.constructor.requiredFields) {
target.constructor.requiredFields = [];
}
target.constructor.requiredFields.push(propertyKey);
};
}
function ValidateClass(target: any) {
const originalConstructor = target;
function construct(constructor: any, args: any[]) {
const instance: any = new constructor(...args);
if (constructor.requiredFields) {
constructor.requiredFields.forEach((field: string) => {
if (!instance[field]) {
throw new Error(`Missing required field: ${field}`);
}
});
}
return instance;
}
const newConstructor: any = function (...args: any[]) {
return construct(originalConstructor, args);
};
newConstructor.prototype = originalConstructor.prototype;
return newConstructor;
}
@ValidateClass
class User {
@Required()
name: string;
@Required()
email: string;
constructor(name: string, email: string) {
this.name = name;
this.email = email;
}
}
try {
const validUser = new User('John Doe', 'john.doe@example.com');
console.log('Valid user created:', validUser);
const invalidUser = new User('Jane Doe', ''); // Missing email
} catch (error) {
console.error('Validation error:', error.message);
}
У цьому прикладі використовуються два декоратори: Required та ValidateClass. Декоратор Required позначає властивості як обов’язкові. Декоратор ValidateClass перехоплює конструктор класу та перевіряє, чи всі обов’язкові поля мають значення. Якщо якесь із обов’язкових полів відсутнє, виникає помилка.
Впровадження залежностей
Як показано в прикладі декоратора параметрів, декоратори можуть полегшити базове впровадження залежностей, полегшуючи керування залежностями та роз’єднання компонентів. Хоча існують більш складні фреймворки впровадження залежностей, декоратори можуть забезпечити легкий і зручний спосіб обробки простих сценаріїв впровадження залежностей.
Рекомендації та найкращі практики
- Розумійте контекст виконання: Пам’ятайте про аргументи
target,propertyKeyтаdescriptor, передані функції декоратора. Ці аргументи надають цінну інформацію про декоровану декларацію та дозволяють відповідним чином змінити її поведінку. - Використовуйте декоратори помірно: Хоча декоратори можуть бути потужними, надмірне використання може призвести до складного та важкозрозумілого коду. Використовуйте декоратори розважливо та лише тоді, коли вони забезпечують явну користь з точки зору повторного використання коду, читабельності чи зручності обслуговування.
- Дотримуйтесь угод про іменування: Використовуйте описові імена для своїх декораторів, щоб чітко вказати їх призначення. Це зробить ваш код більш самодокументованим і зрозумілим.
- Підтримуйте розділення проблем: Декоратори повинні зосереджуватися на конкретних міжфункціональних проблемах і уникати змішування не пов’язаних між собою функцій. Це покращить модульність і зручність обслуговування вашого коду.
- Ретельно тестуйте свої декоратори: Як і будь-який інший код, декоратори слід ретельно тестувати, щоб переконатися, що вони функціонують правильно та не призводять до небажаних побічних ефектів.
- Остерігайтеся побічних ефектів: Декоратори виконуються під час виконання. Уникайте складних або довгострокових операцій у функціях декораторів, оскільки це може вплинути на продуктивність програми.
- Рекомендовано TypeScript: Хоча декоратори JavaScript технічно можна використовувати у звичайному JavaScript із транспіляцією Babel, вони найчастіше використовуються з TypeScript. TypeScript забезпечує чудову безпеку типів і перевірку декораторів під час розробки.
Глобальні перспективи та приклади
Принципи повторного використання коду, зручності обслуговування та розділення проблем, які полегшують декоратори, є загальнозастосовними в різних контекстах розробки програмного забезпечення в усьому світі. Однак конкретні реалізації та варіанти використання можуть відрізнятися залежно від стека технологій, вимог проекту та практик розробки, поширених у різних регіонах.
Наприклад, у корпоративній розробці Java анотації (подібні за концепцією до декораторів) широко використовуються для конфігурації та впровадження залежностей (наприклад, Spring Framework). Хоча синтаксис і основні механізми відрізняються від декораторів JavaScript, основні принципи метапрограмування та AOP залишаються незмінними. Подібним чином у Python декоратори є першокласною функцією мови та часто використовуються для таких завдань, як реєстрація, автентифікація та кешування.
Під час роботи в міжнародних командах або участі у проектах із відкритим кодом із глобальною аудиторією важливо дотримуватися стандартів кодування та найкращих практик, які сприяють ясності та зручності обслуговування. Ефективне використання декораторів може сприяти більш модульній і добре структурованій кодовій базі, полегшуючи розробникам із різним досвідом співпрацю та внесок.
Висновок
Декоратори JavaScript — це потужна та універсальна функція метапрограмування, яка може значно покращити повторне використання коду, читабельність і зручність обслуговування. Забезпечуючи декларативний спосіб додавання метаданих і реалізації принципів AOP, декоратори дозволяють інкапсулювати загальну поведінку, розділяти проблеми та створювати більш модульні та добре структуровані програми. Хоча це все ще пропозиція, що активно розробляється, декоратори вже знайшли широке застосування у таких фреймворках, як Angular та NestJS, і мають усі шанси стати дедалі важливою частиною екосистеми JavaScript. Розуміючи синтаксис, використання та найкращі практики декораторів, ви можете використати їхню потужність для створення більш надійних, масштабованих і зручних у обслуговуванні програм.
Оскільки екосистема JavaScript продовжує розвиватися, постійна обізнаність з новими функціями та найкращими практиками має вирішальне значення для створення високоякісного програмного забезпечення, яке відповідає потребам користувачів у всьому світі. Володіння декораторами JavaScript — це цінна навичка, яка може допомогти вам стати більш ефективним і продуктивним розробником.